#!/usr/bin/python

'''
Created On: April, 17 2025
'''

import os
import sys
import psutil


__NO_ERROR    = "\u001b[32m" # green
__WARNING = "\u001b[33m" # yellow
__FATAL  = "\u001b[31m" # read
__INFO = "\u001b[36m" # cyan
__NONE    = "\u001b[0m"           # reset


def print_usage_and_exit():
    basename = os.path.basename(sys.argv[0])
    print(__WARNING, "Usage: python " + basename, __NONE)
    print(__WARNING, "Usage: python " + basename + " pid0 pid1 pid2 . . .", __NONE)
    print(__WARNING, "Usage: python " + basename + " procname0 procname1 procname2 . . .", __NONE)
    print(__WARNING, "Usage: python " + basename + " procname0 pid0 procname1 pid1 . . .", __NONE)
    exit(0)


def get_process_name(pid: int) -> str | None:
    try:
        with open(f"/proc/{pid}/comm", "r") as f:
            return f.read().strip()
    except (FileNotFoundError, PermissionError):
        return None


def check_valid(proc_entry):
    if not str.isdigit(proc_entry):
        return False

    process_dir_path = os.path.join("/proc", proc_entry)
    if not os.path.isdir(process_dir_path):
        return False

    if not os.access(process_dir_path, os.R_OK) or \
        not os.access(process_dir_path, os.X_OK):
        return False

    cur_uid = os.getuid()
    st = os.stat(process_dir_path)
    if cur_uid != st.st_uid:
        return False

    fdinfo_dir_path = os.path.join(process_dir_path, "fdinfo")
    if not os.access(fdinfo_dir_path, os.R_OK) or \
        not os.access(fdinfo_dir_path, os.X_OK):
        return False

    st = os.stat(fdinfo_dir_path)
    if cur_uid != st.st_uid:
        return False

    accessible = True
    try:
        os.listdir(fdinfo_dir_path)
    except:
        accessible = False

    return accessible


def get_all_proc_list():
    procs = []
    proc_path = "/proc"

    for entry in os.listdir(proc_path):
        if not check_valid(entry):
            continue
        procs.append(entry)

    return procs


def parse_fdinfo(fdinfo):
    fd_info = {}
    fd_infos = fdinfo.split("\n")

    for fd_info_item in fd_infos:
        item_info = fd_info_item.split(":")
        fd_info[item_info[0].strip()] = item_info[1].strip()
    return fd_info


def load_proc_fdinfos(proc):
    proc_fd_infos = {}

    process_name = get_process_name(proc)
    proc_fd_infos["proc_name"] = process_name

    proc_path = "/proc"
    process_dir_path = os.path.join(proc_path, proc)
    fdinfo_dir_path = os.path.join(process_dir_path, "fdinfo")

    for fd in os.listdir(fdinfo_dir_path):
        fdinfo_path = os.path.join(fdinfo_dir_path, fd)

        try:
            with open(fdinfo_path, "r") as f:
                fdinfo = f.read().strip()
        except (FileNotFoundError, PermissionError):
            pass
        if "files" not in proc_fd_infos:
            proc_fd_infos["files"] = {}

        proc_fd_infos["files"][fd] = parse_fdinfo(fdinfo)

    return proc_fd_infos


def get_mount_ids():
    mount_ids = {}
    with open('/proc/self/mountinfo', 'r') as f:
        for line in f:
            fields = line.strip().split()
            mount_ids[fields[0]] = fields[4]
            if "shared" in fields[6]:
                mount_ids[fields[6].split(":")[1]] = fields[4]

    # print(mount_ids)
    return mount_ids


def get_fd_type(mount_ids, fd_info):
    if "eventfd-id" in fd_info:
        return "eventfd"
    if "scm_fds" in fd_info:
        return "unix_domain_socket"
    if  "clockid" in fd_info and "ticks" in fd_info:
        return "timerfd"
    if  "sigmask" in fd_info:
        return "signalfd"
    if  "tfd" in fd_info:
        return "epollfd"
    if  "inotify wd" in fd_info:
        return "inotifyfd"

    mnt_id = fd_info["mnt_id"]
    if mnt_id in mount_ids:
        mount_point = mount_ids[fd_info["mnt_id"]]
        if mount_point == "/dev/pts":
            return "terminal"
        elif mount_point == "/dev":
            return "devfile"
        elif mount_point == "/sys/fs/cgroup":
            return "socket"

    return "normal"


def create_file_index(mount_ids, procs_fdinfos):
    file_index = {}
    fd_key = ""
    specialfd_count = 0

    for pid in procs_fdinfos:
        if "files" not in procs_fdinfos[pid]:
            print("No opened files: ", procs_fdinfos[pid]["proc_name"], pid)
            continue
        fd_infos = procs_fdinfos[pid]["files"]
        for fd in fd_infos:
            fd_info = fd_infos[fd]
            # print(pid, fd, ": ", fd_info)
            fdtype = get_fd_type(mount_ids, fd_info)
            fd_key = fdtype
            fd_key = fd_key + ",mid_" + fd_info["mnt_id"] + ",ino_" + fd_info["ino"]
            if "eventfd-id" in fd_info:
                fd_key += ",evid_" + fd_info["eventfd-id"]

            if fdtype == "epollfd":
                fd_key += "," + str(specialfd_count)
                specialfd_count = specialfd_count + 1
            elif fdtype == "signalfd":
                fd_key += "," + str(specialfd_count)
                specialfd_count = specialfd_count + 1
            elif fdtype == "inotifyfd":
                fd_key += "," + str(specialfd_count)
                specialfd_count = specialfd_count + 1

            if fd_key not in file_index:
                file_index[fd_key] = []
            file_index[fd_key].append((pid, fd))

    return file_index


def get_duplicated_fds_description(mount_ids, procs_fdinfos, fds):
    description = ""
    for (pid, fd) in fds:
        description = description + procs_fdinfos[pid]["proc_name"] + "(" + pid + "): " + fd + ", "
    fd_info = procs_fdinfos[pid]["files"][fd]
    fdtype = get_fd_type(mount_ids, fd_info)
    description = fdtype + ", " + description[:-2]
    return description


def get_running_process_with_name(name):
    processes = []
    for proc in psutil.process_iter(['pid', 'name', 'status']):
        # print(f"PID: {proc.info['pid']}, Process: {proc.info['name']}, Status: {proc.info['status']}")
        if name.strip().lower() in proc.info['name'].lower():
            processes.append(str(proc.info['pid']))

    return processes


def get_proc_list(proclist):
    procs = []
    for proc_item in proclist:
        if str.isdigit(proc_item):
            procs.append(proc_item)
        else:
            processes = get_running_process_with_name(proc_item)
            if processes and len(processes) > 0:
                procs.extend(processes)
    procs = list(set(procs))
    procs = sorted(procs)

    return procs


def main():
    username = os.environ.get("USER", "") or os.environ.get("LOGNAME", "")
    procs = []
    if len(sys.argv) > 1:
        if sys.argv[1] == "-h":
            print_usage_and_exit()
        procs_tmp = get_proc_list(sys.argv[1:])
        for proc_entry in procs_tmp:
            if not check_valid(proc_entry):
                continue
            procs.append(proc_entry)
    else:
        procs = get_all_proc_list()

    mount_ids = get_mount_ids()

    procs_fdinfos = {}
    for proc in procs:
        procs_fdinfos[proc] = load_proc_fdinfos(proc)

    descriptions = []

    file_index = create_file_index(mount_ids, procs_fdinfos)
    for file_key in file_index:
        fds = file_index[file_key]
        if len(fds) > 1:
            descriptions.append(get_duplicated_fds_description(mount_ids, procs_fdinfos, fds))
    descriptions = sorted(descriptions)
    for description in descriptions:
        print("Duplicated Fd: " + description)


if __name__ == "__main__":
    main()
